package org.goobs.mapping;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;

import org.goobs.utils.Pair;
import org.goobs.utils.Util;

public class Road {

	private static int JUNCTION_ID=0;
	
	private double[] lons;
	private double[] lats;
	private Map <String, String> properties = new HashMap <String, String>();
	
	public Road(double[] lons, double[] lats){
		this.lons = lons;
		this.lats = lats;
		if(lons.length != lats.length){
			throw new IllegalArgumentException("longitude and latitude lengths do not match");
		}
	}
	
	public void setProperty(String key, String val){
		properties.put(key, val.trim());
	}
	
	public String getProperty(String key){
		String rtn = properties.get(key);
		if(rtn == null){
			return "";
		}else{
			return rtn;
		}
	}
	
	public int numNodes(){
		return lons.length;
	}
	
	public Set <IntercityRoad>  sweep(Set <City> cities){
		//--Get locations
		HashMap <Integer, City> onRoute = new HashMap <Integer, City> ();
		for(City c : cities){
			Pair <Integer, Double> closest = findClosestNodeToCity(c);
//			System.out.println(c.getName() + "\t" + closest.cdr());
			if(closest.cdr() < c.radius()){
				//(error check for overlapping cities of junctions)
				if(onRoute.containsKey(closest.car())){
					City old = onRoute.get(closest.car());
					if(old.isJunction() && c.isJunction()){
						//TODO this should be an error!
//						throw new IllegalArgumentException("overlapping junctions detected: " + old + "\t" + c);
					}
					if(!old.isJunction() && !c.isJunction()){
						throw new IllegalArgumentException("overlapping cities detected!");
					}
					if(!old.isJunction() && c.isJunction()){
						continue;	//don't overwrite a city with a junction
					}
				}
				//(put the location on the route)
				onRoute.put(closest.car(), c);
			}
		}
		for(City c : onRoute.values()){
			System.out.println("\tcity on route: " + c);
		}
		
		//--Get distances
		//(get the first city)
		HashSet <IntercityRoad> roads = new HashSet <IntercityRoad> ();
		IntercityRoad rolling = null;
		int start = 0;
		while(start < lons.length && rolling == null){
			City c = onRoute.get(start);
			if(c != null){
				rolling = new IntercityRoad(getProperty("ref"), c, null, 0);
			}
			start++;
		}
		//(exit if no cities found along path)
		if(start >= lons.length-1){
			return roads;
		}
		//(get the other roads along the path)
		double rollingDistance = RoadMap.toKilometers(lons[start], lats[start], lons[start+1], lats[start+1]);
		for(int i=start+1; i<lons.length; i++){
			rollingDistance += RoadMap.toKilometers(lons[i-1], lats[i-1], lons[i], lats[i]);
			City c = onRoute.get(i);
			if(c != null){
				rolling.setEnd(c);
				rolling.setDistance(rollingDistance);
				roads.add(rolling);
				rolling = new IntercityRoad(getProperty("ref"), c, null, 0);
				rollingDistance = 0;
			}
		}
		
		//--Reverse roads if relevant
		Set <IntercityRoad> reverses = new HashSet <IntercityRoad> ();
		if(!properties.containsValue("oneway")){
			for(IntercityRoad road : roads){
				IntercityRoad reverse = road.reverse();
				reverses.add(reverse);
			}
		}
		roads.addAll(reverses);
		
		//--Return
		return roads;
	}
	
	private Pair <Integer, Double> findClosestNodeToCity(City c){
		double min = Double.MAX_VALUE;
		int argmin = -1;
		for(int i=0; i<lons.length; i++){
			double lat = lats[i];
			double lon = lons[i];
			double dist = RoadMap.toKilometers(c, lon, lat);
			if( dist < min ){
				min = dist;
				argmin = i;
			}
		}
		return new Pair <Integer, Double> (argmin, min);
	}
	
	public void append(Road b){
		double[] lons = new double[this.lons.length + b.lons.length];
		double[] lats = new double[this.lats.length + b.lats.length];
		for(int i=0; i<this.lons.length; i++){
			lons[i] = this.lons[i];
			lats[i] = this.lats[i];
		}
		for(int i=0; i<b.lons.length; i++){
			lons[this.lons.length+i] = b.lons[i];
			lats[this.lats.length+i] = b.lats[i];
		}
		this.lons = lons;
		this.lats = lats;
	}
	
	public boolean merge(Road other){
		if(this.lons[0] == other.lons[other.lons.length-1] &&
				this.lats[0] == other.lats[other.lats.length-1]){
			//--We start where other left off
			double[] lons = new double[this.lons.length + other.lons.length - 1];
			double[] lats = new double[this.lats.length + other.lats.length - 1];
			for(int i=0; i<other.lons.length; i++){
				lons[i] = other.lons[i];
				lats[i] = other.lats[i];
			}
			for(int i=1; i<this.lons.length; i++){
				lons[other.lons.length+i-1] = this.lons[i];
				lats[other.lats.length+i-1] = this.lats[i];
			}
			this.lons = lons;
			this.lats = lats;
			return true;
		}else if(this.lons[this.lons.length-1] == other.lons[0] &&
				this.lats[this.lats.length-1] == other.lats[0]){
			//--Other starts where we leave off
			double[] lons = new double[this.lons.length + other.lons.length - 1];
			double[] lats = new double[this.lats.length + other.lats.length - 1];
			for(int i=0; i<this.lons.length; i++){
				lons[i] = this.lons[i];
				lats[i] = this.lats[i];
			}
			for(int i=1; i<other.lons.length; i++){
				lons[this.lons.length+i-1] = other.lons[i];
				lats[this.lats.length+i-1] = other.lats[i];
			}
			this.lons = lons;
			this.lats = lats;
			return true;
		}else{
			return false;
		}
	}
	
	public void printCords(){
		for(int i=0; i<lons.length; i++){
			System.out.println("( " + lons[i] + "\t,\t" + lats[i] + " )");
		}
	}
	
	public Road copy(){
		Road r = new Road(lons, lats);
		for(String key : this.properties.keySet()){
			r.setProperty(key, getProperty(key));
		}
		return r;
	}
	
	@Override
	public String toString(){
		String nm = getProperty("ref");
		return nm + "(" + lons.length + " nodes)";
	}
	
//	@Override
//	public boolean equals(Object o){
//		if(o instanceof Road){
//			Road rd = (Road) o;
//			if(rd.lons.length != this.lons.length){
//				return false;
//			}
//			for(int i=0; i<lons.length; i++){
//				if(lons[i] != rd.lons[i] || lats[i] != rd.lats[i]){
//					return false;
//				}
//			}
//			return true;
//		}else{
//			return false;
//		}
//	}
//	
//	@Override
//	public int hashCode(){
//		return (int) (lons[0] * lats[0]);
//	}
	
	
	public static Set <Road> preciseMerge(Set <Road> rawRoads){
		Set <Road> rtn = new HashSet <Road> ();
		for(Road r : rawRoads){ rtn.add(r); }
		
		boolean cont = true;
		while(cont){
			cont = false;
			Set <Road> blacklist = new HashSet <Road>();
			//(merge possible roads)
			for(Road r1 : rtn){
				if(blacklist.contains(r1)){
					continue;
				}
				for(Road r2 : rtn){
					if(r1 == r2 || blacklist.contains(r2)){
						continue;
					}
					if(r1.merge(r2)){
						cont = true;
						blacklist.add(r2);
					}
				}
			}
			//(remove merged roads)
			for(Road r : blacklist){
				rtn.remove(r);
			}	
		}
		
		return rtn;
	}
	
	private static class RoadGap implements Comparable <RoadGap>{
		private Road a, b;
		private double dist;
		private RoadGap(Road a, Road b, double dist){
			this.a = a;
			this.b = b;
			this.dist = dist;
		}
		@Override
		public int compareTo(RoadGap arg0) {
			if(this.dist == arg0.dist){
				return 0;
			}else if(this.dist < arg0.dist){
				return -1;
			}else{
				return 1;
			}
		}	
		public boolean equals(Object o){
			if(o instanceof RoadGap){
				RoadGap other = (RoadGap) o;
				return other.dist == this.dist;
			}else{
				return false;
			}
		}
		public int hashCode(){
			return (int) (dist*1000);
		}
	}
	
	public static Set <Road> softMerge(Set <Road> raw, double maxDist){
		int initSize = 0;
		for(Road r : raw){
			initSize += r.numNodes();
		}
		//--Sort the gaps
		PriorityQueue <RoadGap> gaps = new PriorityQueue<RoadGap> ();
		for(Road r1 : raw){
			for(Road r2 : raw){
				if(r1 == r2){
					continue;
				}
				double latDif = r2.lats[0] - r1.lats[r1.lats.length-1];
				double lonDif = r2.lons[0] - r1.lons[r1.lons.length-1];
				double dif = Util.distance(latDif, lonDif);
				RoadGap gap = new RoadGap(r1, r2, dif);
				gaps.add(gap);
			}
		}
		//--Skim off the smallest gaps
		Set <Road> validRoads = new HashSet <Road> ();
		for(Road r : raw){ validRoads.add(r); }
		Set <Road> blocklist = new HashSet <Road> ();
		double largestSoFar = 0;
		while(!gaps.isEmpty() && validRoads.size() > 1){
			RoadGap gap = gaps.poll();
			//(break if the distance is too large)
			if(gap.dist > maxDist){
				break;
			}
			//(ignore it if gap has been filled)
			if(!validRoads.contains(gap.b) || !validRoads.contains(gap.a)){
				continue;
			}
			//(otherwise, this is the extension)
			gap.a.append(gap.b);
			if(!validRoads.remove(gap.b)){
				throw new IllegalStateException("INTERNAL ERROR");
			}
			//(sanity checking)
			if(gap.dist > 0.1){
				System.out.println("[Road] WARNING: forcing a soft merge over a long distance: " 
						+  gap.a.getProperty("ref") + " " + gap.dist + " : (" 
						+ gap.a.lons[gap.a.lons.length-1] + " , " + gap.a.lats[gap.a.lats.length-1] + ") : ("
						+ gap.b.lons[0] + " , " + gap.b.lats[0] + ")");
			}
			if(gap.dist < largestSoFar){
				throw new IllegalStateException("INTERNAL ERROR: " + gap.dist + " " +largestSoFar);
			}
			largestSoFar = gap.dist;
		}
		
		//--Return the result
		return validRoads;
	}
	
	public static Road hardMerge(Set <Road> raw){
		Set <Road> soft = softMerge(raw, Double.POSITIVE_INFINITY);
		if(soft.size() != 1){
			throw new IllegalStateException("INTERNAL ERROR on hardMerge()");
		}
		Road rtn = null;
		for(Road r : soft){ rtn = r; }
		return rtn;
	}
	
	public static Pair <City, Double> getClosestJunction(Road a, Road b){
		int aIndex = -1;
		int bIndex = -1;
		double dist = Double.POSITIVE_INFINITY;
		
		for(int i=0; i<a.lons.length; i++){
			for(int j=0; j<b.lons.length; j++){
				double lonDif = a.lons[i] - b.lons[j];
				double latDif = a.lats[i] - b.lats[j];
				double d = Util.distance(lonDif, latDif);
				if(d < dist){
					aIndex = i;
					bIndex = j;
					dist = d;
				}
			}
		}
		
		double aveLon = (a.lons[aIndex] + b.lons[bIndex]) / 2.0;
		double aveLat = (a.lats[aIndex] + b.lats[bIndex]) / 2.0;
		String name = a.getProperty("ref") + ":" + b.getProperty("ref") + "_junction_" + JUNCTION_ID++; 
		
		City toReturn = new City(aveLon, aveLat, name, -1);
		toReturn.setAsJunction();
		
		
		return new Pair <City, Double> (toReturn, dist);
	}
	
}
